package it.unibo.refolding.pico;

import it.unibo.refolding.common.CommonThings;
import it.unibo.refolding.common.MyRuntimeException;
import it.unibo.refolding.common.Sleeper;
import it.unibo.refolding.pico.FileMovingWatcher.FileContentAndTargetFile;
import it.unimi.dsi.fastutil.doubles.DoubleList;
import it.unimi.dsi.fastutil.ints.IntList;

import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.apache.commons.lang3.mutable.MutableLong;
import org.apache.commons.lang3.tuple.Pair;

import com.google.common.base.Function;
import com.google.common.base.Preconditions;

public class PlainRefoldingNanoAdapter extends AbstractRefoldingNanoAdapter {

  /**
   * f(4.35965, 4.359655) = -1.1468803992544615e-06
   */
  static final double ALLOWED_ERROR = 1e-5;
  /**
   * It's not recommended to change only ZScanStart, since it has a pretty high
   * relative error: f = lambda x, y: (x - y) / (x + y) * 2
   * f(2.9389999434999997E-9, 2.9389304129630733E-9) = 2.3658169699119777e-05
   */
  static final double Z_SCAN_START_ALLOWED_ERROR = 1e-3;
  private static final double SINGLE_RETRY_PERIOD_S = 5;

  private final int numberOfSamples;

  private static final Logger logger = Logger.getLogger(PlainRefoldingNanoAdapter.class.getName());

  private final double[] scanRates;

  static double[] computeScanRates(String nanoscopeVersion, int samples) {
    final double numerator;
    final int from, to;
    switch (nanoscopeVersion) {
    case "3":
      numerator = 8.71931 * 2048 / samples;
      from = 0;
      to = (int) Math.floor(numerator * 100);
      break;
    case "5":
      numerator = 9.76563 * 4 * 2048 / samples;
      from = 3;
      // I'm not sure what's the upper limit.
      to = (int) Math.floor(numerator * 400);
      break;
    default:
      throw new IllegalArgumentException("Unknown nanoscope version.");
    }

    double[] a = new double[to - from];
    for (int i = from; i < to; ++i) {
      a[to - 1 - i] = numerator / (i + 1);
    }
    assert CommonThings.increasing(a);
    return a;
  }

  private final double pushM;
  private final double hoverM;
  private final double parkingHoverM;

  private double initialParkingPositionM = Double.NaN;
  private double zScriptMinusForceM = Double.NaN;

  private final double breakAwayScanRateHz;
  private final double fetchMoleculeScanRateHz;
  private double latestParkingDeltaM = 0;

  private CurveSpec latestRefoldingCurveSpec;
  private final int extraDelayMillis;

  /**
   * Nanoscope V has problems with (re)setting Capture Continuous. In fact, it
   * seems to behave most of the time, but not always (!), as a toggle, while in
   * Nanoscope III selecting the menu item Capture Continuous was always setting
   * it to Continuous, independently of its previous state. To minimise the
   * dependency on the strange behaviour of the menu item in Nanoscope V, we
   * only used it when absolutely necessary, that is, for "losing" molecules.
   */
  private Capture latestCaptureState;

  enum Capture {
    UNKNOWN, OFF, NEXT, CONTINUOUS
  }

  public PlainRefoldingNanoAdapter(String nanoscopeVersion, int numberOfSamples, double[] revVels,
    double breakAwayRampSizeM, double fetchMoleculeRampSizeM, double fetchMoleculeSurfaceDelayS,
    double pushM, double hoverM, double parkingHoverM, double loseRampSizeM, double loseVelocityMPerS,
    DoubleList allowedTimeRelativeError, Path junctionPointsDir, Sleeper sleeper, Sleeper nanoDriverSleeper,
    FileMovingWatcher fileMovingWatcher) {
    super(
      revVels, breakAwayRampSizeM, fetchMoleculeRampSizeM, fetchMoleculeSurfaceDelayS,
      loseRampSizeM, loseVelocityMPerS, allowedTimeRelativeError, junctionPointsDir, sleeper, nanoDriverSleeper,
      fileMovingWatcher);
    latestCaptureState = Capture.UNKNOWN;
    this.extraDelayMillis = 50; // isVersionThree(nanoscopeVersion) ? 50 : 1500;
    this.numberOfSamples = numberOfSamples;
    this.scanRates = computeScanRates(nanoscopeVersion, numberOfSamples);
    this.pushM = pushM;
    this.hoverM = hoverM;
    this.parkingHoverM = parkingHoverM;
    this.breakAwayScanRateHz = findBestScanRateMatch(
      defaultRevVelMPerS, scanRates, breakAwayRampSizeM);
    this.fetchMoleculeScanRateHz = findBestScanRateMatch(
      defaultRevVelMPerS, scanRates, fetchMoleculeRampSizeM);
  }

  static double findBestScanRateMatch(double velMPerS, double[] scanRates, double rampSizeM) {
    double rateHz = velMPerS / (2 * rampSizeM);
    int index = CommonThings.indexOfNearestHalfEvenInSortedArray(rateHz, scanRates);
    logger.log(
      Level.INFO, "Closest match of {0} nm/s is {1} nm/s for which scanRateHz={2}.", new Object[] {
      velMPerS * 1e9, scanRates[index] * (2 * rampSizeM) * 1e9, scanRates[index]});
    return scanRates[index];
  }

  /**
   * For now, we guarantee that it doesn't change during refold cycles even of
   * different types fully/some.
   */
  private double latestParkingM() {
    return initialParkingPositionM + latestParkingDeltaM;
  }

  @Override
  public PreviousAndCurrentLists breakAway(String prefix, String suffix) {
    /*
     * When the first call fails, we get left with initialParkingPositionM=NaN
     * but zScriptMinusForceM a finite number.
     */
    Preconditions.checkState(!Double.isNaN(zScriptMinusForceM)
      | Double.isNaN(initialParkingPositionM));
    if (Double.isNaN(zScriptMinusForceM)) {
      if (latestCaptureState == Capture.UNKNOWN) {
        nanoDriver.abortCapture();
        latestCaptureState = Capture.OFF;
      }
      // Optional, if we were already in force mode.
      nanoDriver.forceMode();
      // At least 1 nm is required for proper parsing.
      double smallestRampSizeM = 1e-9;
      nanoDriver.rampSizeM(smallestRampSizeM);
      nanoDriver.numberOfSamples(4096);
      nanoDriver.scanRate(2.18);
      nanoDriver.startStripChart();
      CommonThings.sleep((long) (1000 / 2.18) + extraDelayMillis, sleeper);
      Preconditions.checkState(
        EnumSet.of(Capture.CONTINUOUS, Capture.OFF).contains(latestCaptureState),
        latestCaptureState);
      if (latestCaptureState == Capture.OFF) {
        nanoDriver.captureButton();
        // Now it just became Done, which we denote by OFF.
      }
      StripChartPicoData scriptMode = takeExactlyOneStripChartPicoDataAndFailIfMore(
        prefix, "_stripMode");
      nanoDriver.quitStripChart();
      double scriptModeZMedian = CommonThings.median(CommonThings.toDoubleArray(scriptMode.sharedApproach().sharedZArray()));
      try {
        sleeper.sleep(500);
      } catch (InterruptedException e) {
        throw new MyRuntimeException(e.getMessage(), e);
      }
      nanoDriver.single();
      try {
        sleeper.sleep((long) (1000 / 2.18) + extraDelayMillis);
      } catch (InterruptedException e) {
        throw new MyRuntimeException(e.getMessage(), e);
      }
      if (latestCaptureState == Capture.OFF) {
        nanoDriver.captureButton();
        // Now it just became Done, which we denote by OFF.
      }
      PlainPicoData forceMode = takeExactlyOnePlainPicoDataAndFailIfMore(prefix, "_forceMode").plainPicoData;
      double forceModeZMedian = CommonThings.median(CommonThings.toDoubleArray(forceMode.copyOfFullZArray()));
      Preconditions.checkState(forceMode.zLsbToMetresFactor == scriptMode.zLsbToMetresFactor);
      double scriptModeZMedianM = scriptModeZMedian * forceMode.zLsbToMetresFactor;
      double forceModeZMedianM = forceModeZMedian * forceMode.zLsbToMetresFactor;
      zScriptMinusForceM = scriptModeZMedianM - forceModeZMedianM + smallestRampSizeM / 2;
      logger.log(
        Level.INFO, "scriptModeZMedianNm={0} forceModeZMedianNm={1} zScriptMinusForceNm={2}",
        new Object[] {scriptModeZMedianM * 1e9, forceModeZMedianM * 1e9, zScriptMinusForceM * 1e9});
      // TODO: Is it always non-negative?
      Preconditions.checkArgument(zScriptMinusForceM >= 0);
    }
    if (latestRefoldingCurveSpec != null) {
      assert !Double.isNaN(initialParkingPositionM);
      // Stop works even during the Surface Delay.
      nanoDriver.stop();
      try {
        sleeper.sleep((long) (1000 / latestRefoldingCurveSpec.scanRateHz
          + latestRefoldingCurveSpec.surfaceDelayS + extraDelayMillis));
      } catch (InterruptedException e) {
        throw new MyRuntimeException(e.getMessage(), e);
      }
      // Park here for consistency, although not really necessary ...
      // nanoDriver.zScanStartM(latestParkingM() +
      // latestRefoldingCurveSpec.rampSizeM);
    }
    if (!Double.isNaN(initialParkingPositionM)) {
      nanoDriver.zScanStartM(latestParkingM() + parkingHoverM + pushM);
    }
    nanoDriver.scanRate(breakAwayScanRateHz);
    if (Double.isNaN(initialParkingPositionM)) {
      // For now, we only set it the very first time.
      nanoDriver.numberOfSamples(numberOfSamples);
    }
    nanoDriver.rampSizeM(breakAwayRampSizeM);
    nanoDriver.surfaceDelay(0);
    nanoDriver.single();
    final long breakAwayMillis = (long) (1000 / breakAwayScanRateHz) + extraDelayMillis;
    try {
      sleeper.sleep(breakAwayMillis);
    } catch (InterruptedException e) {
      throw new MyRuntimeException(e.getMessage(), e);
    }
    if (latestCaptureState != Capture.CONTINUOUS) {
      // We need to play with capture continuous here because of lose().
      nanoDriver.captureContinuous();
      latestCaptureState = Capture.CONTINUOUS;
    }
    final MutableLong start = new MutableLong(System.nanoTime());
    PreviousAndCurrentLists results;
    if (Double.isNaN(initialParkingPositionM)) {
      PlainPicoDataAndTargetFile a = takeExactlyOnePlainPicoDataAndFailIfMore(prefix, suffix);
      Preconditions.checkState(
        CurveSpec.approxEqual(a.plainPicoData.rampSizeMetres(), breakAwayRampSizeM, ALLOWED_ERROR),
        "%s %s", a.plainPicoData.rampSizeMetres(), breakAwayRampSizeM);
      Preconditions.checkState(a.plainPicoData.rampDelayS == 0);
      results = PreviousAndCurrentLists.of(a);
    } else {
      CurveSpec curveSpec = new CurveSpec(breakAwayScanRateHz, latestParkingM() + parkingHoverM
        + pushM, breakAwayRampSizeM, 0);
      results = readAll(prefix, suffix, curveSpec, new Runnable() {
        @Override
        public void run() {
          long now = System.nanoTime();
          // Intervene if more than 5 seconds passed.
          if ((now - start.longValue()) / 1e9 > SINGLE_RETRY_PERIOD_S) {
            nanoDriver.stop();
            logger.log(Level.INFO, "No breaking away happened in {0} seconds.", SINGLE_RETRY_PERIOD_S);
            Preconditions.checkState(latestCaptureState == Capture.CONTINUOUS);
            nanoDriver.single();
            try {
              sleeper.sleep(breakAwayMillis);
            } catch (InterruptedException e) {
              throw new MyRuntimeException(e.getMessage(), e);
            }
            start.setValue(now);
          }
        }
      });
    }

    Preconditions.checkState(results.current.size() <= 1);
    latestRefoldingCurveSpec = null;
    if (!Double.isNaN(initialParkingPositionM)) {
      /*
       * Not really necessary to park here, but whatever.
       */
      nanoDriver.zScanStartM(latestParkingM() + breakAwayRampSizeM);
    }
    if (Double.isNaN(initialParkingPositionM) & !results.current.isEmpty()) {
      initialParkingPositionM = CommonThings.last(results.current).onlyPlainPicoData().zScanStartMetres()
        - pushM - parkingHoverM;
    }
    return results;
  }

  private void smallDelay() {
    CommonThings.sleep(50, sleeper);
  }

  @Override
  public ParsedAndTargetFile fetchMolecule(double parkingDeltaM, String prefix, String suffix) {
    Preconditions.checkState(latestCaptureState == Capture.CONTINUOUS);
    latestParkingDeltaM = parkingDeltaM;
    latestRefoldingCurveSpec = null;
    nanoDriver.scanRate(fetchMoleculeScanRateHz);
    // Number of samples is fixed for now.
    nanoDriver.zScanStartM(latestParkingM() + parkingHoverM + pushM);
    nanoDriver.rampSizeM(fetchMoleculeRampSizeM);
    nanoDriver.surfaceDelay(fetchMoleculeSurfaceDelayS);
    smallDelay();
    List<Path> moveAllFilesFromCaptureToTarget = fileMovingWatcher.moveAllFilesFromCaptureToTarget(
      prefix, suffix + "_unexpected");
    if (!moveAllFilesFromCaptureToTarget.isEmpty()) {
      logger.log(Level.SEVERE, "Found unexpected files: {0}.", moveAllFilesFromCaptureToTarget);
    }
    nanoDriver.single();
    final long fetchMoleculeMillis = (long) (1000 / fetchMoleculeScanRateHz + fetchMoleculeSurfaceDelayS * 1000) + extraDelayMillis;
    try {
      sleeper.sleep(fetchMoleculeMillis);
    } catch (InterruptedException e) {
      throw new MyRuntimeException("Interrupted while waiting for fetch.", e);
    }
    
    final MutableLong start = new MutableLong(System.nanoTime());
    CurveSpec curveSpec = new CurveSpec(fetchMoleculeScanRateHz, latestParkingM() + parkingHoverM
      + pushM, fetchMoleculeRampSizeM, fetchMoleculeSurfaceDelayS);
    PreviousAndCurrentLists results = readAll(prefix, suffix, curveSpec, new Runnable() {
      @Override
      public void run() {
        long now = System.nanoTime();
        // Intervene if more than 5 seconds passed.
        if ((now - start.longValue()) / 1e9 > SINGLE_RETRY_PERIOD_S) {
          nanoDriver.stop();
          logger.log(Level.INFO, "No fetch molecule happened in {0} seconds.", SINGLE_RETRY_PERIOD_S);
          Preconditions.checkState(latestCaptureState == Capture.CONTINUOUS);
          nanoDriver.single();
          try {
            sleeper.sleep(fetchMoleculeMillis);
          } catch (InterruptedException e) {
            throw new MyRuntimeException("Interrupted while rewaiting for fetch.", e);
          }
          start.setValue(now);
        }
      }
    });
    // Park quickly.
    nanoDriver.zScanStartM(latestParkingM() + fetchMoleculeRampSizeM);
    
    Preconditions.checkState(results.current.size() >= 1);
    if (results.current.size() != 1) {
      logger.log(Level.WARNING, "results.current={0}", results.current);
    }
    return CommonThings.last(results.current);
  }

  @Override
  public NanoDriverConfig createRobotConfig(boolean forceRecord) {
    return NanoDriverUtil.createPlainRefoldingRobotConfig(forceRecord);
  }

  @Override
  public PreviousAndCurrentLists refold(
    int velIndex, double parkingDeltaM, double refoldApproachRampSizeM, double refoldRetractionRampSizeM, double surfaceDelayS,
    String prefix, String suffix) {
    checkApproachAndRetractionRampSizesAreEqual(refoldApproachRampSizeM, refoldRetractionRampSizeM);
    Preconditions.checkState(latestCaptureState == Capture.CONTINUOUS);
    double scanRateHz = findBestScanRateMatch(velIndex == -1 ? defaultRevVelMPerS
      : reverseVelocity(velIndex), scanRates, refoldRetractionRampSizeM);
    // So that latestParkingM() works.
    latestParkingDeltaM = parkingDeltaM;
    CurveSpec newCurveSpec = new CurveSpec(
    /*
     * TODO: refoldScanRate
     */
    scanRateHz, latestParkingM() + parkingHoverM - hoverM, refoldRetractionRampSizeM, surfaceDelayS);

    if (latestRefoldingCurveSpec == null) {
      nanoDriver.scanRate(newCurveSpec.scanRateHz);
      nanoDriver.surfaceDelay(newCurveSpec.surfaceDelayS);
      nanoDriver.rampSizeM(newCurveSpec.rampSizeM);
      // Only need to do it the first time.
      /*
       * Hopefully refoldingRampSizeM > pushM so we don't touch the surface.
       * Even better would be refoldingRampSizeM >= pushM + parkingHoverM so we
       * stay in the safe region. If not, we could do a staggered movement
       * alternating small changes in RampSize and ZScanStart.
       */
      nanoDriver.zScanStartM(newCurveSpec.zScanStartM);
      nanoDriver.continuous();
    } else {
      int differences = latestRefoldingCurveSpec.differences(newCurveSpec);
      if (differences != 0) {
        if (differences != 1) {
          // Don't capture curves with mixed specs.
          nanoDriver.stop();
          // // Park before delay.
          // nanoDriver.zScanStartM(latestParkingM() +
          // latestRefoldingCurveSpec.rampSizeM);
          smallDelay();
        }
        /*
         * Stop works even during the Surface Delay. If we had to wait longer,
         * we'd have to park here by calling:
         * nanoDriver.zScanStartM(latestParkingM() +
         * latestRefoldingCurveSpec.rampSizeM), and after the delay call again:
         * nanoDriver.zScanStartM(latestParkingM() + parkingHoverM - hoverM).
         */
        if (latestRefoldingCurveSpec.scanRateHz != newCurveSpec.scanRateHz) {
          nanoDriver.scanRate(newCurveSpec.scanRateHz);
        }
        if (latestRefoldingCurveSpec.surfaceDelayS != newCurveSpec.surfaceDelayS) {
          nanoDriver.surfaceDelay(newCurveSpec.surfaceDelayS);
        }
        // Assume all ramp sizes are safe from parking position.
        if (latestRefoldingCurveSpec.rampSizeM != newCurveSpec.rampSizeM) {
          nanoDriver.rampSizeM(newCurveSpec.rampSizeM);
        }
        if (latestRefoldingCurveSpec.zScanStartM != newCurveSpec.zScanStartM) {
          // Unpark.
          nanoDriver.zScanStartM(newCurveSpec.zScanStartM);
        }
        if (differences != 1) {
          nanoDriver.continuous();
        }
      }
      // No need to set again ZScanStart.
    }

    try {
      return readAll(prefix, suffix, newCurveSpec, CommonThings.noopRunnable);
    } finally {
      // Not sure if we really need try/finally here.
      latestRefoldingCurveSpec = newCurveSpec;
    }
  }

  private static void checkApproachAndRetractionRampSizesAreEqual(
    double refoldApproachRampSizeM, double refoldRetractionRampSizeM) {
    Preconditions.checkArgument(
      refoldApproachRampSizeM == refoldRetractionRampSizeM, "%s(nm) != %s(nm)",
      refoldRetractionRampSizeM * 1e9, refoldRetractionRampSizeM * 1e9);
  }

  private PlainPicoDataAndTargetFile takeExactlyOnePlainPicoDataAndFailIfMore(
    String prefix, String suffix) {
    List<FileContentAndTargetFile> a = fileMovingWatcher.allFilesIncludingLatest(
      prefix, suffix, CommonThings.noopRunnable);
    Preconditions.checkState(a.size() == 1, a);
    return new PlainPicoDataAndTargetFile(
      (PlainPicoData) PicoDataExtractor.parse(a.get(0).fileContent), a.get(0).targetFile);
  }

  private StripChartPicoData takeExactlyOneStripChartPicoDataAndFailIfMore(
    String prefix, String suffix) {
    List<FileContentAndTargetFile> a = fileMovingWatcher.allFilesIncludingLatest(
      prefix, suffix, CommonThings.noopRunnable);
    Preconditions.checkState(a.size() == 1, a);
    return ((StripChartPicoData) PicoDataExtractor.parse(a.get(0).fileContent));
  }

  private PreviousAndCurrentLists readAll(
    String prefix, String suffix, CurveSpec newCurveSpec, Runnable noFilesRunnable) {
    return readAll(
      prefix,
      suffix,
      latestRefoldingCurveSpec,
      newCurveSpec,
      new Function<FileContentAndTargetFile, PlainPicoData>() {
        @Override
        public PlainPicoData apply(FileContentAndTargetFile input) {
          return (PlainPicoData) PicoDataExtractor.parse(input.fileContent);
        }
      },
      new Function<Pair<List<PlainPicoDataAndTargetFile>, List<PlainPicoDataAndTargetFile>>, Boolean>() {
        @Override
        public Boolean apply(
          Pair<List<PlainPicoDataAndTargetFile>, List<PlainPicoDataAndTargetFile>> input) {
          return !input.getLeft().isEmpty();
        }

      }, noFilesRunnable, fileMovingWatcher);
  }

  static PreviousAndCurrentLists readAll(
    String prefix,
    String suffix,
    CurveSpec latestRefoldingCurveSpec,
    CurveSpec newCurveSpec,
    Function<FileContentAndTargetFile, PlainPicoData> parser,
    Function<Pair<List<PlainPicoDataAndTargetFile>, List<PlainPicoDataAndTargetFile>>, Boolean> stopCondition,
    Runnable noFilesAction, FileMovingWatcher fileMovingWatcher) {
    List<PlainPicoDataAndTargetFile> previous = new ArrayList<>();
    List<PlainPicoDataAndTargetFile> current = new ArrayList<>();
    List<PlainPicoDataAndTargetFile> unparseable = new ArrayList<>();
    do {
      boolean currentWasEmpty = current.isEmpty();
      List<FileContentAndTargetFile> a = fileMovingWatcher.allFilesIncludingLatest(
        prefix, suffix, noFilesAction);
      for (FileContentAndTargetFile x : a) {
        PlainPicoData picoData = parser.apply(x);
        if (picoData != null) {
          CurveSpec picoDataCurveSpec = CurveSpec.extract(picoData);
          boolean compatiblePrevious = latestRefoldingCurveSpec != null
            && latestRefoldingCurveSpec.close(
              picoDataCurveSpec, ALLOWED_ERROR, Z_SCAN_START_ALLOWED_ERROR);
          boolean compatibleCurrent = newCurveSpec.close(
            picoDataCurveSpec, ALLOWED_ERROR, Z_SCAN_START_ALLOWED_ERROR);
          Preconditions.checkState(
            newCurveSpec.equals(latestRefoldingCurveSpec)
              | (compatiblePrevious ^ compatibleCurrent),
            "file=%s picoDataCurveSpec=%s latestRefoldingCurveSpec=%s compatiblePrevious=%s newCurveSpec=%s compatibleCurrent=%s",
            x.targetFile, picoDataCurveSpec, latestRefoldingCurveSpec, compatiblePrevious,
            newCurveSpec, compatibleCurrent);
          /**
           * The result of allFilesIncludingLatest is not necessarily
           * time-ordered, but the results of different invocations are.
           */
          Preconditions.checkState(currentWasEmpty | compatibleCurrent, x.targetFile);
          (compatibleCurrent ? current : previous).add(new PlainPicoDataAndTargetFile(
            picoData, x.targetFile));
        } else {
          unparseable.add(new PlainPicoDataAndTargetFile(null, x.targetFile));
        }
      }
    } while (!stopCondition.apply(Pair.of(current, unparseable)));
    return PreviousAndCurrentLists.of(previous, current, unparseable);
  }

  @Override
  public void xOffsetM(double xOffsetM) {
    // Ignore the old value.
    nanoDriver.xOffsetM(xOffsetM);
  }

  @Override
  public void ping() {
    nanoDriver.ping();
  }

  /**
   * Note that the waiting position in Force Mode is approx 87.5 nm lower than
   * under Force Mode. However, the contact point has the same Z sensor (nm)
   * value in both modes. It means that, probably, when switching on the Script
   * tab, the piezo moves up about 87.5 nm. Indeed, when in contact under Force
   * Mode, clicking on Start the voltage goes up 1.65 V.
   */
  @Override
  public PreviousAndCurrentLists lose(
    double refoldApproachRampSizeM, double refoldRetractionRampSizeM,
    double parkingDeltaM, final String prefix, final String suffix) {
    checkApproachAndRetractionRampSizesAreEqual(refoldApproachRampSizeM, refoldRetractionRampSizeM);
    Preconditions.checkState(latestCaptureState == Capture.CONTINUOUS);
    // So that latestParkingM() works.
    latestParkingDeltaM = parkingDeltaM;
    double newZScanStartM = latestParkingM() + parkingHoverM - hoverM;
    CurveSpec newCurveSpec = new CurveSpec(
      Double.NaN, newZScanStartM, refoldRetractionRampSizeM, PicoConstants.DEAD_TIME_S);
    nanoDriver.stop();
    smallDelay();

    /*
     * In the future we could just change zScanStart taking into account also
     * the difference between the 2 possibly different refolding ramp sizes.
     */
    // Assume all ramp sizes are safe from parking position.
    if (latestRefoldingCurveSpec == null
      || latestRefoldingCurveSpec.rampSizeM != refoldRetractionRampSizeM) {
      nanoDriver.rampSizeM(newCurveSpec.rampSizeM);
    }
    if (latestRefoldingCurveSpec == null || latestRefoldingCurveSpec.zScanStartM != newZScanStartM) {
      nanoDriver.zScanStartM(newCurveSpec.zScanStartM);
    }

    final double approachRampSizeM = refoldRetractionRampSizeM - zScriptMinusForceM;
    Preconditions.checkArgument(approachRampSizeM >= 0, "really?");
    double expectedTimeS = approachRampSizeM / loseVelocityMPerS + PicoConstants.DEAD_TIME_S
      + loseRampSizeM / loseVelocityMPerS;
    double accurateTimeS = PicoConstants.DEAD_TIME_S + expectedTimeS + PicoConstants.DEAD_TIME_S;
    nanoDriver.scriptMode();
    final PreviousAndCurrentLists results;
    try {
      Preconditions.checkState(latestCaptureState == Capture.CONTINUOUS);
      nanoDriver.abortCapture();
      latestCaptureState = Capture.OFF;
      /*
       * singleScript presses the Capture button, but also consumes it, so at
       * the end it should be Done.
       */
      /*
       * If you press directly the Run button, Nanoscope V does not register the
       * first 256 ms exactly.
       */
      nanoDriver.startStripChart();
      CommonThings.sleep(512, sleeper);
      PlainRefoldingNanoAdapter.singleScript(
        0, approachRampSizeM, loseVelocityMPerS, newCurveSpec.surfaceDelayS, loseVelocityMPerS,
        loseRampSizeM, accurateTimeS, prefix, nanoDriver);
      results = readAll(
        prefix,
        suffix,
        latestRefoldingCurveSpec,
        CurveSpec.allNaN,
        new Function<FileContentAndTargetFile, PlainPicoData>() {
          @Override
          public PlainPicoData apply(FileContentAndTargetFile input) {
            PicoData picoData = PicoDataExtractor.parse(input.fileContent);
            if (picoData instanceof PlainPicoData) {
              return (PlainPicoData) picoData;
            } else {
              StripChartPicoData stripChart = (StripChartPicoData) picoData;
              IntList jp;
              try {
                jp = StripChartRefoldingNanoAdapter.readOrWriteApproachRetractionLoopJunctionPoints(
                  stripChart, 0, stripChart.timeInstantCount(), input.targetFile, new LoopParams(
                    approachRampSizeM, loseVelocityMPerS, PicoConstants.DEAD_TIME_S,
                  loseRampSizeM, loseVelocityMPerS, 0, 1, true), allowedTimeRelativeError,
                  junctionPointsDir);
              } catch (IOException e) {
                throw new MyRuntimeException(e.getMessage(), e);
              }
              Parsed parsed = StripChartRefoldingNanoAdapter.parseApproachRetraction(
                stripChart, jp, prefix, suffix);
              Preconditions.checkArgument(parsed.picoDataList.size() <= 1);
              if (parsed == Parsed.NOT_PARSED) {
                return null;
              } else {
                return parsed.picoDataList.get(0).withZScanStartM(Double.NaN).withSurfaceDelayS(
                  Double.NaN);
              }
            }
          }
        },
        new Function<Pair<List<PlainPicoDataAndTargetFile>, List<PlainPicoDataAndTargetFile>>, Boolean>() {
          @Override
          public Boolean apply(
            Pair<List<PlainPicoDataAndTargetFile>, List<PlainPicoDataAndTargetFile>> input) {
            return !input.getLeft().isEmpty() | !input.getRight().isEmpty();
          }
        }, CommonThings.noopRunnable, fileMovingWatcher);
      assert latestCaptureState == Capture.OFF;
      nanoDriver.captureContinuous();
      latestCaptureState = Capture.CONTINUOUS;
    } finally {
      nanoDriver.quitStripChart();
      nanoDriver.forceMode();
    }
    latestRefoldingCurveSpec = newCurveSpec;
    return results;
  }

  static void singleScript(
  	double initialMovementM, double approachRampSizeM, double approachVelocityMPerS,
  	double surfaceDelayS, double retractionVelocityMPerS, double retractionRampSizeM,
  	double expectedTimeS, String prefix,
  	NanoDriver nanoDriver) {
    StripChartRefoldingNanoAdapter.setupSingleScriptAndRun(
      new SingleScriptSetup(initialMovementM, approachRampSizeM, approachVelocityMPerS, surfaceDelayS,
      retractionVelocityMPerS, retractionRampSizeM, 0, 0, 1), prefix, nanoDriver);
  	nanoDriver.sleep(expectedTimeS);
  	nanoDriver.waitWhileStartOrPauseIsNotRed();
  	nanoDriver.captureButton();
  }
}
